package com.example.springboot.common;

import com.example.springboot.config.FilterRescorer;
import com.example.springboot.utils.RecommendPreference;
import lombok.extern.slf4j.Slf4j;
import org.apache.mahout.cf.taste.common.Refreshable;
import org.apache.mahout.cf.taste.common.TasteException;
import org.apache.mahout.cf.taste.eval.IRStatistics;
import org.apache.mahout.cf.taste.eval.RecommenderBuilder;
import org.apache.mahout.cf.taste.eval.RecommenderEvaluator;
import org.apache.mahout.cf.taste.eval.RecommenderIRStatsEvaluator;
import org.apache.mahout.cf.taste.impl.common.FastByIDMap;
import org.apache.mahout.cf.taste.impl.eval.AverageAbsoluteDifferenceRecommenderEvaluator;
import org.apache.mahout.cf.taste.impl.eval.GenericRecommenderIRStatsEvaluator;
import org.apache.mahout.cf.taste.impl.eval.RMSRecommenderEvaluator;
import org.apache.mahout.cf.taste.impl.model.GenericDataModel;
import org.apache.mahout.cf.taste.impl.model.GenericPreference;
import org.apache.mahout.cf.taste.impl.model.GenericUserPreferenceArray;
import org.apache.mahout.cf.taste.impl.neighborhood.NearestNUserNeighborhood;
import org.apache.mahout.cf.taste.impl.neighborhood.ThresholdUserNeighborhood;
import org.apache.mahout.cf.taste.impl.recommender.GenericBooleanPrefItemBasedRecommender;
import org.apache.mahout.cf.taste.impl.recommender.GenericBooleanPrefUserBasedRecommender;
import org.apache.mahout.cf.taste.impl.recommender.GenericItemBasedRecommender;
import org.apache.mahout.cf.taste.impl.recommender.GenericUserBasedRecommender;
import org.apache.mahout.cf.taste.impl.similarity.*;
import org.apache.mahout.cf.taste.model.DataModel;
import org.apache.mahout.cf.taste.model.PreferenceArray;
import org.apache.mahout.cf.taste.neighborhood.UserNeighborhood;
import org.apache.mahout.cf.taste.recommender.RecommendedItem;
import org.apache.mahout.cf.taste.similarity.ItemSimilarity;
import org.apache.mahout.cf.taste.similarity.UserSimilarity;
import org.springframework.lang.NonNull;

import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Slf4j
public class Recommender {

    private final DataModel dataModel;

    private Recommender(DataModel dataModel) {
        this.dataModel = dataModel;
    }

    public static Recommender build(List<? extends RecommendPreference> preferenceList) {
        DataModel model = buildJdbcDataModel(preferenceList);
        return new Recommender(model);
    }

    /**
     * dataModel 有两种结构：
     * GenericDataModel: 用户ID，物品ID，用户对物品的打分(UserID,ItemID,PreferenceValue)
     * GenericBooleanPrefDataModel: 用户ID，物品ID (UserID,ItemID)，这种方式表达用户是否浏览过该物品，但并未对物品进行打分。
     * 因为系统需要根据用户行为或评分进行推荐所以使用GenericDataModel
     *
     * @param preferenceList 用户行为或评分集合
     * @return DataModel
     */
    private static DataModel buildJdbcDataModel(List<? extends RecommendPreference> preferenceList) {
        FastByIDMap<PreferenceArray> fastByIdMap = new FastByIDMap<>();
        Map<Long, List<RecommendPreference>> map = preferenceList.stream().collect(Collectors.groupingBy(RecommendPreference::getUserId));
        Collection<List<RecommendPreference>> list = map.values();
        for (List<RecommendPreference> preferences : list) {
            GenericPreference[] array = new GenericPreference[preferences.size()];
            for (int i = 0; i < preferences.size(); i++) {
                RecommendPreference preference = preferences.get(i);
                GenericPreference item = new GenericPreference(preference.getUserId(), preference.getItemId(), preference.getValue());
                array[i] = item;
            }
            fastByIdMap.put(array[0].getUserID(), new GenericUserPreferenceArray(Arrays.asList(array)));
        }
        return new GenericDataModel(fastByIdMap);
    }

    /**
     * 创建基于用户的推荐器
     *
     * @param similarityType 相似度算法类型
     * @return UserBaseRecommender
     */
    public UserBaseRecommender userBaseRecommender(String similarityType) throws TasteException {

        return new UserBaseRecommender(similarityType);
    }

    /**
     * 创建基于物品的推荐器
     *
     * @param similarityType 相似度算法类型
     * @return UserBaseRecommender
     */
    public ItemBaseRecommender itemBaseRecommender(String similarityType) throws TasteException {

        return new ItemBaseRecommender(similarityType);
    }

    public class UserBaseRecommender {

        private final UserSimilarity similarity;

        private UserNeighborhood neighborhood;

        public UserBaseRecommender(String similarity) throws TasteException {
            this.similarity = userSimilarity(similarity, dataModel);
        }

        private UserSimilarity userSimilarity(String type, DataModel m) throws TasteException {
            return (UserSimilarity) getSimilarity(type, m);
        }

        /**
         * 计算最近邻域 基于固定数量的邻居,对每个用户取固定数量N个最近邻居
         *
         * @param num 固定邻居数量
         * @return UserNeighborhood
         */
        public UserRecommenderBuilder nearestUserNeighborhood(int num) throws TasteException {
            neighborhood = new NearestNUserNeighborhood(num, similarity, dataModel);
            return new UserRecommenderBuilder(similarity, neighborhood);
        }

        /**
         * 计算最近邻域 基于相似度,对每个用户基于一定的限制，取落在相似度限制以内的所有用户为邻居
         *
         * @param num 最低相似度
         * @return UserNeighborhood
         */
        public UserRecommenderBuilder thresholdUserNeighborhood(double num) throws TasteException {
            neighborhood = new ThresholdUserNeighborhood(num, similarity, dataModel);
            return new UserRecommenderBuilder(similarity, neighborhood);
        }


    }

    public class UserRecommenderBuilder {

        private final UserSimilarity similarity;

        private final UserNeighborhood neighborhood;

        private UserRecommenderBuilder(@NonNull UserSimilarity similarity, @NonNull UserNeighborhood neighborhood) {
            this.similarity = similarity;
            this.neighborhood = neighborhood;
        }

        /**
         * 获取基于用户的推荐器
         *
         * @param pref 是否需要首选,即偏好
         * @return RecommenderBuilder
         */
        public SimpleRecommender recommender(boolean pref) throws TasteException {
            return new SimpleRecommender(pref
                    //基于用户的推荐引擎
                    ? model -> new GenericUserBasedRecommender(model, neighborhood, similarity)
                    //基于用户的无偏好值推荐引擎
                    : model -> new GenericBooleanPrefUserBasedRecommender(model, neighborhood, similarity));
        }
    }

    class ItemBaseRecommender {
        private final ItemSimilarity similarity;

        public ItemBaseRecommender(String similarity) throws TasteException {
            this.similarity = itemSimilarity(similarity, dataModel);
        }

        public ItemSimilarity itemSimilarity(String type, DataModel m) throws TasteException {
            return (ItemSimilarity) getSimilarity(type, m);
        }

        /**
         * 获取基于用户的推荐器
         *
         * @param pref 是否需要首选
         * @return RecommenderBuilder
         */
        public SimpleRecommender recommender(boolean pref) throws TasteException {
            return new SimpleRecommender(pref
                    //基于物品的推荐引擎
                    ? model -> new GenericItemBasedRecommender(model, similarity)
                    //基于物品的无偏好值推荐引擎
                    : model -> new GenericBooleanPrefItemBasedRecommender(model, similarity));
        }
    }

    private static Refreshable getSimilarity(String type, DataModel m) throws TasteException {
        switch (type) {
            case RecommenderConstants.SIMILARITY_PEARSON:
                return new PearsonCorrelationSimilarity(m);
            case RecommenderConstants.SIMILARITY_COSINE:
                return new UncenteredCosineSimilarity(m);
            case RecommenderConstants.SIMILARITY_TANIMOTO:
                return new TanimotoCoefficientSimilarity(m);
            case RecommenderConstants.SIMILARITY_LOGLIKELIHOOD:
                return new LogLikelihoodSimilarity(m);
            case RecommenderConstants.SIMILARITY_CITY_BLOCK:
                return new CityBlockSimilarity(m);
            case RecommenderConstants.SIMILARITY_SPEARMAN:
                return new SpearmanCorrelationSimilarity(m);
            case RecommenderConstants.SIMILARITY_EUCLIDEAN:
            default:
                return new SpearmanCorrelationSimilarity(m);
        }
    }

    public class SimpleRecommender {
        private final RecommenderBuilder builder;
        private final org.apache.mahout.cf.taste.recommender.Recommender recommender;

        private SimpleRecommender(RecommenderBuilder builder) throws TasteException {
            this.builder = builder;
            this.recommender = builder.buildRecommender(dataModel);
        }

        public List<RecommendedItem> recommend(long userId, int howMany) throws TasteException {
            return recommender.recommend(userId, howMany);
        }

        public List<RecommendedItem> recommend(long userId, int howMany, FilterRescorer rescorer) throws TasteException {
            return recommender.recommend(userId, howMany, rescorer);
        }

        /**
         * 计算用户的预测评分和实际评分之间的“均方根”差异。这是这个差值的平均值的平方根
         */
        public Evaluator rmsEvaluator() {
            return new Evaluator(builder, new RMSRecommenderEvaluator());
        }

        /**
         * 计算平均差值的比较器
         */
        public Evaluator averageAbsoluteDifferenceEvaluator() {
            return new Evaluator(builder, new AverageAbsoluteDifferenceRecommenderEvaluator());
        }
    }

    public class Evaluator {
        private final RecommenderBuilder builder;
        private final RecommenderEvaluator recommenderEvaluator;

        private Evaluator(RecommenderBuilder builder, RecommenderEvaluator recommenderEvaluator) {
            this.builder = builder;
            this.recommenderEvaluator = recommenderEvaluator;
        }

        /**
         * 推荐器进度评估，数值越低精度越高
         * 返回值越小，推荐算法越好，0是最低/最好的评估值，意味着完美匹配。
         *
         * @param trainPt 培训数据占比
         */
        public double evaluate(double trainPt) throws TasteException {
            return recommenderEvaluator.evaluate(builder, null, dataModel, trainPt, 1.0);
        }

        public IRStatistics statsEvaluator(int topn) throws TasteException {
            RecommenderIRStatsEvaluator evaluator = new GenericRecommenderIRStatsEvaluator();
            return evaluator.evaluate(builder, null, dataModel, null, topn, GenericRecommenderIRStatsEvaluator.CHOOSE_THRESHOLD, 1.0);
        }
    }
}